Finish documenting monero-serai

This commit is contained in:
Luke Parker
2024-06-22 14:37:43 -04:00
parent 1db40914eb
commit 74aaac46ef
12 changed files with 435 additions and 360 deletions

View File

@@ -270,7 +270,7 @@ pub trait Rpc: Sync + Clone + Debug {
async fn get_protocol(&self) -> Result<u8, RpcError> { async fn get_protocol(&self) -> Result<u8, RpcError> {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct ProtocolResponse { struct ProtocolResponse {
major_version: u8, hardfork_version: u8,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@@ -283,7 +283,7 @@ pub trait Rpc: Sync + Clone + Debug {
.json_rpc_call::<LastHeaderResponse>("get_last_block_header", None) .json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
.await? .await?
.block_header .block_header
.major_version, .hardfork_version,
) )
} }

View File

@@ -114,7 +114,7 @@ mod binaries {
); );
assert_eq!(tx.hash(), tx_hash, "Transaction hash was different"); assert_eq!(tx.hash(), tx_hash, "Transaction hash was different");
if matches!(tx.rct_signatures.prunable, RctPrunable::Null) { if matches!(tx.proofs.prunable, RctPrunable::Null) {
assert_eq!(tx.prefix.version, 1); assert_eq!(tx.prefix.version, 1);
assert!(!tx.signatures.is_empty()); assert!(!tx.signatures.is_empty());
continue; continue;
@@ -126,7 +126,7 @@ mod binaries {
// multisig explicitly calling verify as part of its signing process // multisig explicitly calling verify as part of its signing process
// Accordingly, making sure our signature_hash algorithm is correct is great, and further // Accordingly, making sure our signature_hash algorithm is correct is great, and further
// making sure the verification functions are valid is appreciated // making sure the verification functions are valid is appreciated
match tx.rct_signatures.prunable { match tx.proofs.prunable {
RctPrunable::Null | RctPrunable::Null |
RctPrunable::AggregateMlsagBorromean { .. } | RctPrunable::AggregateMlsagBorromean { .. } |
RctPrunable::MlsagBorromean { .. } => {} RctPrunable::MlsagBorromean { .. } => {}
@@ -134,14 +134,14 @@ mod binaries {
assert!(bulletproofs.batch_verify( assert!(bulletproofs.batch_verify(
&mut rand_core::OsRng, &mut rand_core::OsRng,
&mut batch, &mut batch,
&tx.rct_signatures.base.commitments &tx.proofs.base.commitments
)); ));
} }
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
assert!(bulletproofs.batch_verify( assert!(bulletproofs.batch_verify(
&mut rand_core::OsRng, &mut rand_core::OsRng,
&mut batch, &mut batch,
&tx.rct_signatures.base.commitments &tx.proofs.base.commitments
)); ));
for (i, clsag) in clsags.into_iter().enumerate() { for (i, clsag) in clsags.into_iter().enumerate() {

View File

@@ -18,10 +18,14 @@ const EXISTING_BLOCK_HASH_202612: [u8; 32] =
/// A Monero block's header. /// 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. /// The hard fork of the protocol this block follows.
pub major_version: u8, ///
/// The minor version of the protocol. /// Per the C++ codebase, this is the `major_version`.
pub minor_version: u8, pub hardfork_version: u8,
/// A signal for a proposed hard fork.
///
/// Per the C++ codebase, this is the `minor_version`.
pub hardfork_signal: u8,
/// Seconds since the epoch. /// Seconds since the epoch.
pub timestamp: u64, pub timestamp: u64,
/// The previous block's hash. /// The previous block's hash.
@@ -36,8 +40,8 @@ pub struct BlockHeader {
impl BlockHeader { impl BlockHeader {
/// Write the 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.hardfork_version, w)?;
write_varint(&self.minor_version, w)?; write_varint(&self.hardfork_signal, w)?;
write_varint(&self.timestamp, w)?; write_varint(&self.timestamp, w)?;
w.write_all(&self.previous)?; w.write_all(&self.previous)?;
w.write_all(&self.nonce.to_le_bytes()) w.write_all(&self.nonce.to_le_bytes())
@@ -53,8 +57,8 @@ impl BlockHeader {
/// Read a BlockHeader. /// 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)?, hardfork_version: read_varint(r)?,
minor_version: read_varint(r)?, hardfork_signal: read_varint(r)?,
timestamp: read_varint(r)?, timestamp: read_varint(r)?,
previous: read_bytes(r)?, previous: read_bytes(r)?,
nonce: read_bytes(r).map(u32::from_le_bytes)?, nonce: read_bytes(r).map(u32::from_le_bytes)?,
@@ -79,13 +83,15 @@ impl Block {
/// This information comes from the Block's miner transaction. If the miner transaction isn't /// This information comes from the Block's miner transaction. If the miner transaction isn't
/// structed as expected, this will return None. /// 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 {
Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => match prefix.inputs.first() {
Some(Input::Gen(number)) => Some(*number), Some(Input::Gen(number)) => Some(*number),
_ => None, _ => None,
} }
} }
}
/// Write the BlockHeader. /// Write the Block.
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)?;
@@ -96,7 +102,7 @@ impl Block {
Ok(()) Ok(())
} }
/// Serialize the BlockHeader to a Vec<u8>. /// Serialize the Block 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();
@@ -116,10 +122,10 @@ impl Block {
/// Get the hash of this block. /// 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_pow_hash();
// Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash // Monero pre-appends a VarInt of the block-to-hash'ss length before getting the block hash,
// but doesn't do this when getting the proof of work hash :) // but doesn't do this when getting the proof of work hash :)
let mut hashing_blob = Vec::with_capacity(8 + hashable.len()); let mut hashing_blob = Vec::with_capacity(9 + hashable.len());
write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap(); write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap();
hashing_blob.append(&mut hashable); hashing_blob.append(&mut hashable);
@@ -130,18 +136,11 @@ impl Block {
hash hash
} }
/// Read a BlockHeader. /// Read a Block.
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 miner_tx = Transaction::read(r)?;
if !matches!(miner_tx.prefix.inputs.as_slice(), &[Input::Gen(_)]) {
Err(io::Error::other("Miner transaction has incorrect input type."))?;
}
Ok(Block { Ok(Block {
header, header: BlockHeader::read(r)?,
miner_tx, miner_tx: Transaction::read(r)?,
txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?, txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
}) })
} }

View File

@@ -1,6 +1,6 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
// #![deny(missing_docs)] // TODO #![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), no_std)]
pub use monero_io as io; pub use monero_io as io;

View File

@@ -1,12 +1,11 @@
use core::ops::Deref;
use std_shims::{ use std_shims::{
vec::Vec, vec::Vec,
io::{self, Read, Write}, io::{self, Read, Write},
}; };
use zeroize::{Zeroize, Zeroizing}; use zeroize::Zeroize;
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; use curve25519_dalek::edwards::EdwardsPoint;
pub use monero_mlsag as mlsag; pub use monero_mlsag as mlsag;
pub use monero_clsag as clsag; pub use monero_clsag as clsag;
@@ -15,15 +14,24 @@ pub use monero_bulletproofs as bulletproofs;
use crate::{ use crate::{
io::*, io::*,
generators::hash_to_point,
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof}, ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof},
}; };
/// An encrypted amount. /// An encrypted amount.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum EncryptedAmount { pub enum EncryptedAmount {
Original { mask: [u8; 32], amount: [u8; 32] }, /// The original format for encrypted amounts.
Compact { amount: [u8; 8] }, Original {
/// A mask used with a mask derived from the shared secret to encrypt the amount.
mask: [u8; 32],
/// The amount, as a scalar, encrypted.
amount: [u8; 32],
},
/// The "compact" format for encrypted amounts.
Compact {
/// The amount, as a u64, encrypted.
amount: [u8; 8],
},
} }
impl EncryptedAmount { impl EncryptedAmount {
@@ -51,44 +59,41 @@ impl EncryptedAmount {
/// The type of the RingCT data. /// The type of the RingCT data.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum RctType { pub enum RctType {
/// No RCT proofs.
Null,
/// One MLSAG for multiple inputs and Borromean range proofs. /// One MLSAG for multiple inputs and Borromean range proofs.
/// ///
/// This lines up with RCTTypeFull. /// This aligns with RCTTypeFull.
MlsagAggregate, AggregateMlsagBorromean,
// One MLSAG for each input and a Borromean range proof. // One MLSAG for each input and a Borromean range proof.
/// ///
/// This lines up with RCTTypeSimple. /// This aligns with RCTTypeSimple.
MlsagIndividual, MlsagBorromean,
// One MLSAG for each input and a Bulletproof. // One MLSAG for each input and a Bulletproof.
/// ///
/// This lines up with RCTTypeBulletproof. /// This aligns with RCTTypeBulletproof.
Bulletproofs, MlsagBulletproofs,
/// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact. /// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact.
/// ///
/// This lines up with RCTTypeBulletproof2. /// This aligns with RCTTypeBulletproof2.
BulletproofsCompactAmount, MlsagBulletproofsCompactAmount,
/// One CLSAG for each input and a Bulletproof. /// One CLSAG for each input and a Bulletproof.
/// ///
/// This lines up with RCTTypeCLSAG. /// This aligns with RCTTypeCLSAG.
Clsag, ClsagBulletproof,
/// One CLSAG for each input and a Bulletproof+. /// One CLSAG for each input and a Bulletproof+.
/// ///
/// This lines up with RCTTypeBulletproofPlus. /// This aligns with RCTTypeBulletproofPlus.
BulletproofsPlus, ClsagBulletproofPlus,
} }
impl From<RctType> for u8 { impl From<RctType> for u8 {
fn from(kind: RctType) -> u8 { fn from(kind: RctType) -> u8 {
match kind { match kind {
RctType::Null => 0, RctType::AggregateMlsagBorromean => 1,
RctType::MlsagAggregate => 1, RctType::MlsagBorromean => 2,
RctType::MlsagIndividual => 2, RctType::MlsagBulletproofs => 3,
RctType::Bulletproofs => 3, RctType::MlsagBulletproofsCompactAmount => 4,
RctType::BulletproofsCompactAmount => 4, RctType::ClsagBulletproof => 5,
RctType::Clsag => 5, RctType::ClsagBulletproofPlus => 6,
RctType::BulletproofsPlus => 6,
} }
} }
} }
@@ -97,27 +102,51 @@ impl TryFrom<u8> for RctType {
type Error = (); type Error = ();
fn try_from(byte: u8) -> Result<Self, ()> { fn try_from(byte: u8) -> Result<Self, ()> {
Ok(match byte { Ok(match byte {
0 => RctType::Null, 1 => RctType::AggregateMlsagBorromean,
1 => RctType::MlsagAggregate, 2 => RctType::MlsagBorromean,
2 => RctType::MlsagIndividual, 3 => RctType::MlsagBulletproofs,
3 => RctType::Bulletproofs, 4 => RctType::MlsagBulletproofsCompactAmount,
4 => RctType::BulletproofsCompactAmount, 5 => RctType::ClsagBulletproof,
5 => RctType::Clsag, 6 => RctType::ClsagBulletproofPlus,
6 => RctType::BulletproofsPlus,
_ => Err(())?, _ => Err(())?,
}) })
} }
} }
impl RctType { impl RctType {
/// Returns true if this RctType uses compact encrypted amounts, false otherwise. /// True if this RctType uses compact encrypted amounts, false otherwise.
pub fn compact_encrypted_amounts(&self) -> bool { fn compact_encrypted_amounts(&self) -> bool {
match self { match self {
RctType::Null | RctType::AggregateMlsagBorromean | RctType::MlsagBorromean | RctType::MlsagBulletproofs => {
RctType::MlsagAggregate | false
RctType::MlsagIndividual | }
RctType::Bulletproofs => false, RctType::MlsagBulletproofsCompactAmount |
RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => true, RctType::ClsagBulletproof |
RctType::ClsagBulletproofPlus => true,
}
}
/// True if this RctType uses a Bulletproof, false otherwise.
pub(crate) fn bulletproof(&self) -> bool {
match self {
RctType::MlsagBulletproofs |
RctType::MlsagBulletproofsCompactAmount |
RctType::ClsagBulletproof => true,
RctType::AggregateMlsagBorromean |
RctType::MlsagBorromean |
RctType::ClsagBulletproofPlus => false,
}
}
/// True if this RctType uses a Bulletproof+, false otherwise.
pub(crate) fn bulletproof_plus(&self) -> bool {
match self {
RctType::ClsagBulletproofPlus => true,
RctType::AggregateMlsagBorromean |
RctType::MlsagBorromean |
RctType::MlsagBulletproofs |
RctType::MlsagBulletproofsCompactAmount |
RctType::ClsagBulletproof => false,
} }
} }
} }
@@ -137,7 +166,7 @@ pub struct RctBase {
/// ///
/// This field was deprecated and is empty for modern RctTypes. /// 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 recipients to decrypt.
pub encrypted_amounts: Vec<EncryptedAmount>, pub encrypted_amounts: Vec<EncryptedAmount>,
/// The output commitments. /// The output commitments.
pub commitments: Vec<EdwardsPoint>, pub commitments: Vec<EdwardsPoint>,
@@ -153,11 +182,9 @@ impl RctBase {
/// Write the RctBase. /// 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(&[u8::from(rct_type)])?; w.write_all(&[u8::from(rct_type)])?;
match rct_type {
RctType::Null => Ok(()),
_ => {
write_varint(&self.fee, w)?; write_varint(&self.fee, w)?;
if rct_type == RctType::MlsagIndividual { if rct_type == RctType::MlsagBorromean {
write_raw_vec(write_point, &self.pseudo_outs, w)?; write_raw_vec(write_point, &self.pseudo_outs, w)?;
} }
for encrypted_amount in &self.encrypted_amounts { for encrypted_amount in &self.encrypted_amounts {
@@ -165,20 +192,18 @@ impl RctBase {
} }
write_raw_vec(write_point, &self.commitments, w) write_raw_vec(write_point, &self.commitments, w)
} }
}
}
/// Read a RctBase. /// 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::try_from(read_byte(r)?).map_err(|_| 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::AggregateMlsagBorromean | RctType::MlsagBorromean => {}
RctType::Bulletproofs | RctType::MlsagBulletproofs |
RctType::BulletproofsCompactAmount | RctType::MlsagBulletproofsCompactAmount |
RctType::Clsag | RctType::ClsagBulletproof |
RctType::BulletproofsPlus => { RctType::ClsagBulletproofPlus => {
if outputs == 0 { if outputs == 0 {
// Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if // Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if
// Bulletproofs are in use // Bulletproofs are in use
@@ -191,14 +216,11 @@ impl RctBase {
} }
Ok(( Ok((
if rct_type == RctType::Null {
RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] }
} else {
RctBase { RctBase {
fee: read_varint(r)?, fee: read_varint(r)?,
// Only read pseudo_outs if they have yet to be moved to RctPrunable // Only read pseudo_outs if they have yet to be moved to RctPrunable
// TODO: Shouldn't this be any Mlsag*? // TODO: Shouldn't this be any Mlsag*?
pseudo_outs: if rct_type == RctType::MlsagIndividual { pseudo_outs: if rct_type == RctType::MlsagBorromean {
read_raw_vec(read_point, inputs, r)? read_raw_vec(read_point, inputs, r)?
} else { } else {
vec![] vec![]
@@ -207,7 +229,6 @@ impl RctBase {
.map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r)) .map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r))
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
commitments: read_raw_vec(read_point, outputs, r)?, commitments: read_raw_vec(read_point, outputs, r)?,
}
}, },
rct_type, rct_type,
)) ))
@@ -217,20 +238,50 @@ impl RctBase {
/// The prunable part of the RingCT data. /// 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,
/// An aggregate MLSAG with Borromean range proofs. /// An aggregate MLSAG with Borromean range proofs.
AggregateMlsagBorromean { borromean: Vec<BorromeanRange>, mlsag: Mlsag }, AggregateMlsagBorromean {
/// The aggregate MLSAG ring signature.
mlsag: Mlsag,
/// The Borromean range proofs for each output.
borromean: Vec<BorromeanRange>,
},
/// MLSAGs with Borromean range proofs. /// MLSAGs with Borromean range proofs.
MlsagBorromean { borromean: Vec<BorromeanRange>, mlsags: Vec<Mlsag> }, MlsagBorromean {
/// The MLSAG ring signatures for each input.
mlsags: Vec<Mlsag>,
/// The Borromean range proofs for each output.
borromean: Vec<BorromeanRange>,
},
/// MLSAGs with Bulletproofs. /// MLSAGs with Bulletproofs.
MlsagBulletproofs { MlsagBulletproofs {
bulletproofs: Bulletproof, /// The MLSAG ring signatures for each input.
mlsags: Vec<Mlsag>, mlsags: Vec<Mlsag>,
/// The re-blinded commitments for the outputs being spent.
pseudo_outs: Vec<EdwardsPoint>, pseudo_outs: Vec<EdwardsPoint>,
/// The aggregate Bulletproof, proving the outputs are within range.
bulletproof: Bulletproof,
},
/// MLSAGs with Bulletproofs and compact encrypted amounts.
///
/// This has an identical layout to MlsagBulletproofs and is interpreted the exact same way. It's
/// only differentiated to ensure discovery of the correct RctType.
MlsagBulletproofsCompactAmount {
/// The MLSAG ring signatures for each input.
mlsags: Vec<Mlsag>,
/// The re-blinded commitments for the outputs being spent.
pseudo_outs: Vec<EdwardsPoint>,
/// The aggregate Bulletproof, proving the outputs are within range.
bulletproof: Bulletproof,
}, },
/// CLSAGs with Bulletproofs(+). /// CLSAGs with Bulletproofs(+).
Clsag { bulletproofs: Bulletproof, clsags: Vec<Clsag>, pseudo_outs: Vec<EdwardsPoint> }, Clsag {
/// The CLSAGs for each input.
clsags: Vec<Clsag>,
/// The re-blinded commitments for the outputs being spent.
pseudo_outs: Vec<EdwardsPoint>,
/// The aggregate Bulletproof(+), proving the outputs are within range.
bulletproof: Bulletproof,
},
} }
impl RctPrunable { impl RctPrunable {
@@ -247,7 +298,6 @@ impl RctPrunable {
/// Write the RctPrunable. /// 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::AggregateMlsagBorromean { borromean, mlsag } => { RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => {
write_raw_vec(BorromeanRange::write, borromean, w)?; write_raw_vec(BorromeanRange::write, borromean, w)?;
mlsag.write(w) mlsag.write(w)
@@ -256,20 +306,21 @@ impl RctPrunable {
write_raw_vec(BorromeanRange::write, borromean, w)?; write_raw_vec(BorromeanRange::write, borromean, w)?;
write_raw_vec(Mlsag::write, mlsags, w) write_raw_vec(Mlsag::write, mlsags, w)
} }
RctPrunable::MlsagBulletproofs { bulletproofs, mlsags, pseudo_outs } => { RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs } |
if rct_type == RctType::Bulletproofs { RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs } => {
if rct_type == RctType::MlsagBulletproofs {
w.write_all(&1u32.to_le_bytes())?; w.write_all(&1u32.to_le_bytes())?;
} else { } else {
w.write_all(&[1])?; w.write_all(&[1])?;
} }
bulletproofs.write(w)?; bulletproof.write(w)?;
write_raw_vec(Mlsag::write, mlsags, w)?; write_raw_vec(Mlsag::write, mlsags, w)?;
write_raw_vec(write_point, pseudo_outs, w) write_raw_vec(write_point, pseudo_outs, w)
} }
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => {
w.write_all(&[1])?; w.write_all(&[1])?;
bulletproofs.write(w)?; bulletproof.write(w)?;
write_raw_vec(Clsag::write, clsags, w)?; write_raw_vec(Clsag::write, clsags, w)?;
write_raw_vec(write_point, pseudo_outs, w) write_raw_vec(write_point, pseudo_outs, w)
@@ -293,19 +344,17 @@ impl RctPrunable {
r: &mut R, r: &mut R,
) -> io::Result<RctPrunable> { ) -> io::Result<RctPrunable> {
Ok(match rct_type { Ok(match rct_type {
RctType::Null => RctPrunable::Null, RctType::AggregateMlsagBorromean => RctPrunable::AggregateMlsagBorromean {
RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean {
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
mlsag: Mlsag::read(ring_length, inputs + 1, r)?, mlsag: Mlsag::read(ring_length, inputs + 1, r)?,
}, },
RctType::MlsagIndividual => RctPrunable::MlsagBorromean { RctType::MlsagBorromean => RctPrunable::MlsagBorromean {
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?, mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?,
}, },
RctType::Bulletproofs | RctType::BulletproofsCompactAmount => { RctType::MlsagBulletproofs | RctType::MlsagBulletproofsCompactAmount => {
RctPrunable::MlsagBulletproofs { let bulletproof = {
bulletproofs: { if (if rct_type == RctType::MlsagBulletproofs {
if (if rct_type == RctType::Bulletproofs {
u64::from(read_u32(r)?) u64::from(read_u32(r)?)
} else { } else {
read_varint(r)? read_varint(r)?
@@ -314,19 +363,27 @@ impl RctPrunable {
Err(io::Error::other("n bulletproofs instead of one"))?; Err(io::Error::other("n bulletproofs instead of one"))?;
} }
Bulletproof::read(r)? Bulletproof::read(r)?
}, };
mlsags: (0 .. inputs) let mlsags =
.map(|_| Mlsag::read(ring_length, 2, r)) (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::<Result<_, _>>()?;
.collect::<Result<_, _>>()?, let pseudo_outs = read_raw_vec(read_point, inputs, r)?;
pseudo_outs: read_raw_vec(read_point, inputs, r)?, if rct_type == RctType::MlsagBulletproofs {
RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs }
} else {
debug_assert_eq!(rct_type, RctType::MlsagBulletproofsCompactAmount);
RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs }
} }
} }
RctType::Clsag | RctType::BulletproofsPlus => RctPrunable::Clsag { RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => RctPrunable::Clsag {
bulletproofs: { bulletproof: {
if read_varint::<_, u64>(r)? != 1 { if read_varint::<_, u64>(r)? != 1 {
Err(io::Error::other("n bulletproofs instead of one"))?; Err(io::Error::other("n bulletproofs instead of one"))?;
} }
(if rct_type == RctType::Clsag { Bulletproof::read } else { Bulletproof::read_plus })(r)? (if rct_type == RctType::ClsagBulletproof {
Bulletproof::read
} else {
Bulletproof::read_plus
})(r)?
}, },
clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::<Result<_, _>>()?, clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::<Result<_, _>>()?,
pseudo_outs: read_raw_vec(read_point, inputs, r)?, pseudo_outs: read_raw_vec(read_point, inputs, r)?,
@@ -335,59 +392,51 @@ impl RctPrunable {
} }
/// Write the RctPrunable as necessary for signing the signature. /// Write the RctPrunable as necessary for signing the signature.
/// pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
/// This function will return None if the object is `RctPrunable::Null` (and has no match self {
/// 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 { bulletproof, .. } |
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w), RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } |
}) RctPrunable::Clsag { bulletproof, .. } => bulletproof.signature_write(w),
}
} }
} }
/// The RingCT proofs.
///
/// This contains both the RctBase and RctPrunable structs.
///
/// The C++ codebase refers to this as rct_signatures.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct RctSignatures { pub struct RctProofs {
/// The data necessary for handling this transaction.
pub base: RctBase, pub base: RctBase,
/// The data necessary for verifying this transaction.
pub prunable: RctPrunable, pub prunable: RctPrunable,
} }
impl RctSignatures { impl RctProofs {
/// RctType for a given RctSignatures struct. /// RctType for a given RctProofs struct.
/// pub fn rct_type(&self) -> RctType {
/// This is only guaranteed to return the type for a well-formed RctSignatures. For a malformed match &self.prunable {
/// RctSignatures, this will return either the presumed RctType (with no guarantee of compliance RctPrunable::AggregateMlsagBorromean { .. } => RctType::AggregateMlsagBorromean,
/// with that type) or None. RctPrunable::MlsagBorromean { .. } => RctType::MlsagBorromean,
#[must_use] RctPrunable::MlsagBulletproofs { .. } => RctType::MlsagBulletproofs,
pub fn rct_type(&self) -> Option<RctType> { RctPrunable::MlsagBulletproofsCompactAmount { .. } => RctType::MlsagBulletproofsCompactAmount,
Some(match &self.prunable { RctPrunable::Clsag { bulletproof, .. } => {
RctPrunable::Null => RctType::Null, if matches!(bulletproof, Bulletproof::Original { .. }) {
RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate, RctType::ClsagBulletproof
RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual,
RctPrunable::MlsagBulletproofs { .. } => {
if matches!(self.base.encrypted_amounts.first()?, EncryptedAmount::Original { .. }) {
RctType::Bulletproofs
} else { } else {
RctType::BulletproofsCompactAmount RctType::ClsagBulletproofPlus
} }
} }
RctPrunable::Clsag { bulletproofs, .. } => {
if matches!(bulletproofs, Bulletproof::Original { .. }) {
RctType::Clsag
} else {
RctType::BulletproofsPlus
} }
} }
})
}
/// The weight of this RctSignatures as relevant for fees. /// The weight of this RctProofs, as relevant for fees.
pub fn fee_weight( pub fn fee_weight(
bp_plus: bool, bp_plus: bool,
ring_len: usize, ring_len: usize,
@@ -398,30 +447,29 @@ 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)
} }
#[must_use] /// Write the RctProofs.
pub fn write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
let rct_type = self.rct_type()?; let rct_type = self.rct_type();
if let Err(e) = self.base.write(w, rct_type) { self.base.write(w, rct_type)?;
return Some(Err(e)); self.prunable.write(w, rct_type)
};
Some(self.prunable.write(w, rct_type))
} }
#[must_use] /// Serialize the RctProofs to a Vec<u8>.
pub fn serialize(&self) -> Option<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();
Some(serialized) serialized
} }
/// Read a RctProofs.
pub fn read<R: Read>( pub fn read<R: Read>(
ring_length: usize, ring_length: usize,
inputs: usize, inputs: usize,
outputs: usize, outputs: usize,
r: &mut R, r: &mut R,
) -> io::Result<RctSignatures> { ) -> io::Result<RctProofs> {
let base = RctBase::read(inputs, outputs, r)?; let base = RctBase::read(inputs, outputs, r)?;
Ok(RctSignatures { Ok(RctProofs {
base: base.0, base: base.0,
prunable: RctPrunable::read(base.1, ring_length, inputs, outputs, r)?, prunable: RctPrunable::read(base.1, ring_length, inputs, outputs, r)?,
}) })

View File

@@ -12,22 +12,34 @@ use crate::{
io::*, io::*,
primitives::keccak256, primitives::keccak256,
ring_signatures::RingSignature, ring_signatures::RingSignature,
ringct::{bulletproofs::Bulletproof, RctType, RctBase, RctPrunable, RctSignatures}, ringct::{bulletproofs::Bulletproof, RctProofs},
}; };
/// An input in the Monero protocol.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Input { pub enum Input {
/// An input for a miner transaction, which is generating new coins.
Gen(u64), Gen(u64),
ToKey { amount: Option<u64>, key_offsets: Vec<u64>, key_image: EdwardsPoint }, /// An input spending an output on-chain.
ToKey {
/// The pool this input spends an output of.
amount: Option<u64>,
/// The decoys used by this input's ring, specified as their offset distance from each other.
key_offsets: Vec<u64>,
/// The key image (linking tag, nullifer) for the spent output.
key_image: EdwardsPoint,
},
} }
impl Input { impl Input {
/// The weight of this Input, as relevant for fees.
pub fn fee_weight(offsets_weight: usize) -> usize { pub fn fee_weight(offsets_weight: usize) -> usize {
// Uses 1 byte for the input type // Uses 1 byte for the input type
// Uses 1 byte for the VarInt amount due to amount being 0 // Uses 1 byte for the VarInt amount due to amount being 0
1 + 1 + offsets_weight + 32 1 + 1 + offsets_weight + 32
} }
/// Write the Input.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
match self { match self {
Input::Gen(height) => { Input::Gen(height) => {
@@ -44,12 +56,14 @@ impl Input {
} }
} }
/// Serialize the Input to a Vec<u8>.
pub fn serialize(&self) -> Vec<u8> { pub fn serialize(&self) -> Vec<u8> {
let mut res = vec![]; let mut res = vec![];
self.write(&mut res).unwrap(); self.write(&mut res).unwrap();
res res
} }
/// Read an Input.
pub fn read<R: Read>(r: &mut R) -> io::Result<Input> { pub fn read<R: Read>(r: &mut R) -> io::Result<Input> {
Ok(match read_byte(r)? { Ok(match read_byte(r)? {
255 => Input::Gen(read_varint(r)?), 255 => Input::Gen(read_varint(r)?),
@@ -72,21 +86,26 @@ impl Input {
} }
} }
// Doesn't bother moving to an enum for the unused Script classes /// An output in the Monero protocol.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct Output { pub struct Output {
/// The pool this output should be sorted into.
pub amount: Option<u64>, pub amount: Option<u64>,
/// The key which can spend this output.
pub key: CompressedEdwardsY, pub key: CompressedEdwardsY,
/// The view tag for this output, as used to accelerate scanning.
pub view_tag: Option<u8>, pub view_tag: Option<u8>,
} }
impl Output { impl Output {
/// The weight of this Output, as relevant for fees.
pub fn fee_weight(view_tags: bool) -> usize { pub fn fee_weight(view_tags: bool) -> usize {
// Uses 1 byte for the output type // Uses 1 byte for the output type
// Uses 1 byte for the VarInt amount due to amount being 0 // Uses 1 byte for the VarInt amount due to amount being 0
1 + 1 + 32 + if view_tags { 1 } else { 0 } 1 + 1 + 32 + if view_tags { 1 } else { 0 }
} }
/// Write the Output.
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.amount.unwrap_or(0), w)?; write_varint(&self.amount.unwrap_or(0), w)?;
w.write_all(&[2 + u8::from(self.view_tag.is_some())])?; w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
@@ -97,12 +116,14 @@ impl Output {
Ok(()) Ok(())
} }
/// Write the Output to a Vec<u8>.
pub fn serialize(&self) -> Vec<u8> { pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(8 + 1 + 32); let mut res = Vec::with_capacity(8 + 1 + 32);
self.write(&mut res).unwrap(); self.write(&mut res).unwrap();
res res
} }
/// Read an Output.
pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> { pub fn read<R: Read>(rct: bool, r: &mut R) -> io::Result<Output> {
let amount = read_varint(r)?; let amount = read_varint(r)?;
let amount = if rct { let amount = if rct {
@@ -128,10 +149,17 @@ impl Output {
} }
} }
/// An additional timelock for a Monero transaction.
///
/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the
/// longer of the two will be the timelock used.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
pub enum Timelock { pub enum Timelock {
/// No timelock.
None, None,
/// Locked until this block.
Block(usize), Block(usize),
/// Locked until this many seconds since the epoch.
Time(u64), Time(u64),
} }
@@ -173,16 +201,27 @@ impl PartialOrd for Timelock {
} }
} }
/// The transaction prefix.
///
/// This is common to all transaction versions and contains most parts of the transaction needed to
/// handle it. It excludes any proofs.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct TransactionPrefix { pub struct TransactionPrefix {
pub version: u64, /// The timelock this transaction uses.
pub timelock: Timelock, pub timelock: Timelock,
/// The inputs for this transaction.
pub inputs: Vec<Input>, pub inputs: Vec<Input>,
/// The outputs for this transaction.
pub outputs: Vec<Output>, pub outputs: Vec<Output>,
/// The additional data included within the transaction.
///
/// This is an arbitrary data field, yet is used by wallets for containing the data necessary to
/// scan the transaction.
pub extra: Vec<u8>, pub extra: Vec<u8>,
} }
impl TransactionPrefix { impl TransactionPrefix {
/// The weight of this TransactionPrefix, as relevant for fees.
pub fn fee_weight( pub fn fee_weight(
decoy_weights: &[usize], decoy_weights: &[usize],
outputs: usize, outputs: usize,
@@ -191,8 +230,7 @@ impl TransactionPrefix {
) -> usize { ) -> usize {
// Assumes Timelock::None since this library won't let you create a TX with a timelock // Assumes Timelock::None since this library won't let you create a TX with a timelock
// 1 input for every decoy weight // 1 input for every decoy weight
1 + 1 + 1 + varint_len(decoy_weights.len()) +
varint_len(decoy_weights.len()) +
decoy_weights.iter().map(|&offsets_weight| Input::fee_weight(offsets_weight)).sum::<usize>() + decoy_weights.iter().map(|&offsets_weight| Input::fee_weight(offsets_weight)).sum::<usize>() +
varint_len(outputs) + varint_len(outputs) +
(outputs * Output::fee_weight(view_tags)) + (outputs * Output::fee_weight(view_tags)) +
@@ -200,8 +238,10 @@ impl TransactionPrefix {
extra extra
} }
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { /// Write a TransactionPrefix.
write_varint(&self.version, w)?; ///
/// This is distinct from Monero in that it won't write any version.
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
self.timelock.write(w)?; self.timelock.write(w)?;
write_vec(Input::write, &self.inputs, w)?; write_vec(Input::write, &self.inputs, w)?;
write_vec(Output::write, &self.outputs, w)?; write_vec(Output::write, &self.outputs, w)?;
@@ -209,19 +249,11 @@ impl TransactionPrefix {
w.write_all(&self.extra) w.write_all(&self.extra)
} }
pub fn serialize(&self) -> Vec<u8> { /// Read a TransactionPrefix.
let mut res = vec![]; ///
self.write(&mut res).unwrap(); /// This is distinct from Monero in that it won't read the version. The version must be passed
res /// in.
} pub fn read<R: Read>(r: &mut R, version: u64) -> io::Result<TransactionPrefix> {
pub fn read<R: Read>(r: &mut R) -> io::Result<TransactionPrefix> {
let version = read_varint(r)?;
// TODO: Create an enum out of version
if (version == 0) || (version > 2) {
Err(io::Error::other("unrecognized transaction version"))?;
}
let timelock = Timelock::from_raw(read_varint(r)?); let timelock = Timelock::from_raw(read_varint(r)?);
let inputs = read_vec(|r| Input::read(r), r)?; let inputs = read_vec(|r| Input::read(r), r)?;
@@ -231,7 +263,6 @@ impl TransactionPrefix {
let is_miner_tx = matches!(inputs[0], Input::Gen { .. }); let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
let mut prefix = TransactionPrefix { let mut prefix = TransactionPrefix {
version,
timelock, timelock,
inputs, inputs,
outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?, outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?,
@@ -241,20 +272,36 @@ impl TransactionPrefix {
Ok(prefix) Ok(prefix)
} }
pub fn hash(&self) -> [u8; 32] { fn hash(&self, version: u64) -> [u8; 32] {
keccak256(self.serialize()) let mut buf = vec![];
write_varint(&version, &mut buf).unwrap();
self.write(&mut buf).unwrap();
keccak256(buf)
} }
} }
/// Monero transaction. For version 1, rct_signatures still contains an accurate fee value. /// A Monero transaction.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct Transaction { pub enum Transaction {
pub prefix: TransactionPrefix, /// A version 1 transaction, used by the original Cryptonote codebase.
pub signatures: Vec<RingSignature>, V1 {
pub rct_signatures: RctSignatures, /// The transaction's prefix.
prefix: TransactionPrefix,
/// The transaction's ring signatures.
signatures: Vec<RingSignature>,
},
/// A version 2 transaction, used by the RingCT protocol.
V2 {
/// The transaction's prefix.
prefix: TransactionPrefix,
/// The transaction's proofs.
proofs: Option<RctProofs>,
},
} }
impl Transaction { impl Transaction {
/// The weight of this Transaction, as relevant for fees.
// TODO: Replace ring_len, decoy_weights for &[&[usize]], where the inner buf is the decoy // TODO: Replace ring_len, decoy_weights for &[&[usize]], where the inner buf is the decoy
// offsets // offsets
pub fn fee_weight( pub fn fee_weight(
@@ -266,88 +313,58 @@ impl Transaction {
extra: usize, extra: usize,
fee: u64, fee: u64,
) -> usize { ) -> usize {
TransactionPrefix::fee_weight(decoy_weights, outputs, view_tags, extra) + 1 + TransactionPrefix::fee_weight(decoy_weights, outputs, view_tags, extra) +
RctSignatures::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee) RctProofs::fee_weight(bp_plus, ring_len, decoy_weights.len(), outputs, fee)
} }
#[must_use] /// Write the Transaction.
pub fn write<W: Write>(&self, w: &mut W) -> Option<io::Result<()>> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
if let Err(e) = self.prefix.write(w) { match self {
return Some(Err(e)); Transaction::V1 { prefix, signatures } => {
}; write_varint(&1u8, w)?;
if self.prefix.version == 1 { prefix.write(w)?;
for ring_sig in &self.signatures { for ring_sig in signatures {
if let Err(e) = ring_sig.write(w) { ring_sig.write(w)?;
return Some(Err(e));
};
} }
Some(Ok(()))
} else if self.prefix.version == 2 {
if let Err(e) = self.rct_signatures.write(w)? {
return Some(Err(e));
} }
Some(Ok(())) Transaction::V2 { prefix, proofs } => {
} else { write_varint(&2u8, w)?;
Some(Err(io::Error::other("transaction had an unknown version"))) prefix.write(w)?;
match proofs {
None => w.write_all(&[0])?,
Some(proofs) => proofs.write(w)?,
} }
} }
}
Ok(())
}
#[must_use] /// Write the Transaction to a Vec<u8>.
pub fn serialize(&self) -> Option<Vec<u8>> { pub fn serialize(&self) -> 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();
Some(res) res
} }
/// Read a Transaction.
pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> { pub fn read<R: Read>(r: &mut R) -> io::Result<Transaction> {
let prefix = TransactionPrefix::read(r)?; let version = read_varint(r)?;
let mut signatures = vec![]; let prefix = TransactionPrefix::read(r, version)?;
let mut rct_signatures = RctSignatures {
base: RctBase { fee: 0, encrypted_amounts: vec![], pseudo_outs: vec![], commitments: vec![] },
prunable: RctPrunable::Null,
};
if prefix.version == 1 { if version == 1 {
signatures = prefix let signatures = prefix
.inputs .inputs
.iter() .iter()
.filter_map(|input| match input { .filter_map(|input| match input {
// TODO: This allows mixing Gen and ToKey, which is likely undefined behavior?
Input::ToKey { key_offsets, .. } => Some(RingSignature::read(key_offsets.len(), r)), Input::ToKey { key_offsets, .. } => Some(RingSignature::read(key_offsets.len(), r)),
_ => None, _ => None,
}) })
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
if !matches!(prefix.inputs[0], Input::Gen(..)) { Ok(Transaction::V1 { prefix, signatures })
let in_amount = prefix } else if version == 2 {
.inputs let proofs = Some(RctProofs::read(
.iter()
.map(|input| match input {
Input::Gen(..) => Err(io::Error::other("Input::Gen present in non-coinbase v1 TX"))?,
// v1 TXs can burn v2 outputs
// dcff3fe4f914d6b6bd4a5b800cc4cca8f2fdd1bd73352f0700d463d36812f328 is one such TX
// It includes a pre-RCT signature for a RCT output, yet if you interpret the RCT
// output as being worth 0, it passes a sum check (guaranteed since no outputs are RCT)
Input::ToKey { amount, .. } => Ok(amount.unwrap_or(0)),
})
.collect::<io::Result<Vec<_>>>()?
.into_iter()
.sum::<u64>();
let mut out = 0;
for output in &prefix.outputs {
if output.amount.is_none() {
Err(io::Error::other("v1 transaction had a 0-amount output"))?;
}
out += output.amount.unwrap();
}
if in_amount < out {
Err(io::Error::other("transaction spent more than it had as inputs"))?;
}
rct_signatures.base.fee = in_amount - out;
}
} else if prefix.version == 2 {
rct_signatures = RctSignatures::read(
prefix.inputs.first().map_or(0, |input| match input { prefix.inputs.first().map_or(0, |input| match input {
Input::Gen(_) => 0, Input::Gen(_) => 0,
Input::ToKey { key_offsets, .. } => key_offsets.len(), Input::ToKey { key_offsets, .. } => key_offsets.len(),
@@ -355,98 +372,109 @@ impl Transaction {
prefix.inputs.len(), prefix.inputs.len(),
prefix.outputs.len(), prefix.outputs.len(),
r, r,
)?; )?);
Ok(Transaction::V2 { prefix, proofs })
} else { } else {
Err(io::Error::other("Tried to deserialize unknown version"))?; Err(io::Error::other("tried to deserialize unknown version"))
}
} }
Ok(Transaction { prefix, signatures, rct_signatures }) /// The hash of the transaction.
} 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 { match self {
self.write(&mut buf)?.unwrap(); Transaction::V1 { .. } => {
Some(keccak256(buf)) self.write(&mut buf).unwrap();
} else { keccak256(buf)
}
Transaction::V2 { prefix, proofs } => {
let mut hashes = Vec::with_capacity(96); let mut hashes = Vec::with_capacity(96);
hashes.extend(self.prefix.hash()); hashes.extend(prefix.hash(2));
let rct_type = self.rct_signatures.rct_type()?; if let Some(proofs) = proofs {
self.rct_signatures.base.write(&mut buf, rct_type).unwrap(); let rct_type = proofs.rct_type();
proofs.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 { proofs.prunable.write(&mut buf, rct_type).unwrap();
RctPrunable::Null => [0; 32], hashes.extend(keccak256(buf));
_ => { } else {
self.rct_signatures.prunable.write(&mut buf, rct_type).unwrap(); // Serialization of RctBase::Null
keccak256(buf) hashes.extend(keccak256([0]));
hashes.extend([0; 32]);
} }
});
Some(keccak256(hashes)) keccak256(hashes)
}
} }
} }
/// Calculate the hash of this transaction as needed for signing it. /// Calculate the hash of this transaction as needed for signing it.
#[must_use] ///
/// This returns None if the transaction is without signatures.
pub fn signature_hash(&self) -> Option<[u8; 32]> { pub fn signature_hash(&self) -> Option<[u8; 32]> {
if self.prefix.version == 1 { match self {
return Some(self.prefix.hash()); Transaction::V1 { prefix, .. } => Some(prefix.hash(1)),
} Transaction::V2 { prefix, proofs } => {
let mut buf = Vec::with_capacity(2048); let mut buf = Vec::with_capacity(2048);
let mut sig_hash = Vec::with_capacity(96); let mut sig_hash = Vec::with_capacity(96);
sig_hash.extend(self.prefix.hash()); sig_hash.extend(prefix.hash(2));
self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()?).unwrap(); let proofs = proofs.as_ref()?;
proofs.base.write(&mut buf, proofs.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(); proofs.prunable.signature_write(&mut buf).unwrap();
sig_hash.extend(keccak256(buf)); sig_hash.extend(keccak256(buf));
Some(keccak256(sig_hash)) Some(keccak256(sig_hash))
} }
}
}
fn is_rct_bulletproof(&self) -> bool { fn is_rct_bulletproof(&self) -> bool {
let Some(rct_type) = self.rct_signatures.rct_type() else { return false }; match self {
match rct_type { Transaction::V1 { .. } => false,
RctType::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag => true, Transaction::V2 { proofs, .. } => {
RctType::Null | let Some(proofs) = proofs else { return false };
RctType::MlsagAggregate | proofs.rct_type().bulletproof()
RctType::MlsagIndividual | }
RctType::BulletproofsPlus => false,
} }
} }
fn is_rct_bulletproof_plus(&self) -> bool { fn is_rct_bulletproof_plus(&self) -> bool {
let Some(rct_type) = self.rct_signatures.rct_type() else { return false }; match self {
match rct_type { Transaction::V1 { .. } => false,
RctType::BulletproofsPlus => true, Transaction::V2 { proofs, .. } => {
RctType::Null | let Some(proofs) = proofs else { return false };
RctType::MlsagAggregate | proofs.rct_type().bulletproof_plus()
RctType::MlsagIndividual | }
RctType::Bulletproofs |
RctType::BulletproofsCompactAmount |
RctType::Clsag => false,
} }
} }
/// Calculate the transaction's weight. /// Calculate the transaction's weight.
pub fn weight(&self) -> Option<usize> { pub fn weight(&self) -> 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();
Some(if !(bp || bp_plus) { 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,
match self {
Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"),
Transaction::V2 { prefix, .. } => prefix.outputs.len(),
},
)
.0
}
} }
} }

View File

@@ -419,7 +419,7 @@ impl Scanner {
commitment.amount = amount; commitment.amount = amount;
// Regular transaction // Regular transaction
} else { } else {
commitment = match tx.rct_signatures.base.encrypted_amounts.get(o) { commitment = match tx.proofs.base.encrypted_amounts.get(o) {
Some(amount) => amount.decrypt(shared_key), Some(amount) => amount.decrypt(shared_key),
// This should never happen, yet it may be possible with miner transactions? // This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case // Using get just decreases the possibility of a panic and lets us move on in that case
@@ -428,7 +428,7 @@ impl Scanner {
// If this is a malicious commitment, move to the next output // If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable // Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) { if Some(&commitment.calculate()) != tx.proofs.base.commitments.get(o) {
break; break;
} }
} }

View File

@@ -30,7 +30,7 @@ use monero_serai::{
hash_to_point, hash_to_point,
clsag::{ClsagError, ClsagContext, Clsag}, clsag::{ClsagError, ClsagContext, Clsag},
bulletproofs::{MAX_COMMITMENTS, Bulletproof}, bulletproofs::{MAX_COMMITMENTS, Bulletproof},
RctBase, RctPrunable, RctSignatures, RctBase, RctPrunable, RctProofs,
}, },
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
}; };
@@ -809,7 +809,7 @@ impl SignableTransaction {
extra, extra,
}, },
signatures: vec![], signatures: vec![],
rct_signatures: RctSignatures { proofs: RctProofs {
base: RctBase { base: RctBase {
fee, fee,
encrypted_amounts, encrypted_amounts,
@@ -855,7 +855,7 @@ impl SignableTransaction {
let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash()) let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash())
.map_err(|_| TransactionError::WrongPrivateKey)?; .map_err(|_| TransactionError::WrongPrivateKey)?;
match tx.rct_signatures.prunable { match tx.proofs.prunable {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"), RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>()); clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>());
@@ -867,7 +867,7 @@ impl SignableTransaction {
if self.has_change { if self.has_change {
debug_assert_eq!( debug_assert_eq!(
self.fee_rate.calculate_fee_from_weight(tx.weight()), self.fee_rate.calculate_fee_from_weight(tx.weight()),
tx.rct_signatures.base.fee, tx.proofs.base.fee,
"transaction used unexpected fee", "transaction used unexpected fee",
); );
} }
@@ -917,7 +917,7 @@ impl Eventuality {
uniqueness(&tx.prefix.inputs), uniqueness(&tx.prefix.inputs),
); );
let rct_type = tx.rct_signatures.rct_type(); let rct_type = tx.proofs.rct_type();
if rct_type != self.protocol.optimal_rct_type() { if rct_type != self.protocol.optimal_rct_type() {
return false; return false;
} }
@@ -935,9 +935,9 @@ impl Eventuality {
key: expected.dest.compress(), key: expected.dest.compress(),
view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()), view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()),
} != actual) || } != actual) ||
(Some(&expected.commitment.calculate()) != tx.rct_signatures.base.commitments.get(o)) || (Some(&expected.commitment.calculate()) != tx.proofs.base.commitments.get(o)) ||
(Some(&EncryptedAmount::Compact { amount: expected.amount }) != (Some(&EncryptedAmount::Compact { amount: expected.amount }) !=
tx.rct_signatures.base.encrypted_amounts.get(o)) tx.proofs.base.encrypted_amounts.get(o))
{ {
return false; return false;
} }

View File

@@ -390,7 +390,7 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine {
shares: HashMap<Participant, Self::SignatureShare>, shares: HashMap<Participant, Self::SignatureShare>,
) -> Result<Transaction, FrostError> { ) -> Result<Transaction, FrostError> {
let mut tx = self.tx; let mut tx = self.tx;
match tx.rct_signatures.prunable { match tx.proofs.prunable {
RctPrunable::Null => panic!("Signing for RctPrunable::Null"), RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
for (c, clsag) in self.clsags.drain(..).enumerate() { for (c, clsag) in self.clsags.drain(..).enumerate() {

View File

@@ -70,7 +70,7 @@ test!(
assert!(eventuality.matches(&tx)); assert!(eventuality.matches(&tx));
// Mutate the TX // Mutate the TX
tx.rct_signatures.base.commitments[0] += ED25519_BASEPOINT_POINT; tx.proofs.base.commitments[0] += ED25519_BASEPOINT_POINT;
// Verify it no longer matches // Verify it no longer matches
assert!(!eventuality.matches(&tx)); assert!(!eventuality.matches(&tx));
}, },

View File

@@ -80,7 +80,7 @@ pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> Spe
/// Make sure the weight and fee match the expected calculation. /// Make sure the weight and fee match the expected calculation.
pub fn check_weight_and_fee(tx: &Transaction, fee_rate: FeeRate) { pub fn check_weight_and_fee(tx: &Transaction, fee_rate: FeeRate) {
let fee = tx.rct_signatures.base.fee; let fee = tx.proofs.base.fee;
let weight = tx.weight(); let weight = tx.weight();
let expected_weight = fee_rate.calculate_weight_from_fee(fee); let expected_weight = fee_rate.calculate_weight_from_fee(fee);

View File

@@ -306,7 +306,7 @@ test!(
assert_eq!(outputs[1].commitment().amount, 50000); assert_eq!(outputs[1].commitment().amount, 50000);
// The remainder should get shunted to fee, which is fingerprintable // The remainder should get shunted to fee, which is fingerprintable
assert_eq!(tx.rct_signatures.base.fee, 1000000000000 - 10000 - 50000); assert_eq!(tx.proofs.base.fee, 1000000000000 - 10000 - 50000);
}, },
), ),
); );