use core::{ops::Deref as _, fmt};
use std_shims::{
io, vec,
vec::Vec,
string::{String, ToString as _},
collections::HashSet,
};
use subtle::ConstantTimeEq as _;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use rand_core::{RngCore, CryptoRng};
use rand::seq::SliceRandom as _;
#[cfg(feature = "compile-time-generators")]
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
#[cfg(not(feature = "compile-time-generators"))]
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT as ED25519_BASEPOINT_TABLE;
#[cfg(feature = "multisig")]
use frost::FrostError;
use crate::{
io::*,
ed25519::*,
ringct::{
clsag::{ClsagError, ClsagContext, Clsag},
bulletproofs::MAX_COMMITMENTS as MAX_BULLETPROOF_COMMITMENTS,
RctType, RctPrunable, RctProofs,
},
transaction::{TransactionPrefix, Transaction},
address::{Network, SubaddressIndex, MoneroAddress},
extra::{MAX_ARBITRARY_DATA_SIZE, MAX_EXTRA_SIZE_BY_RELAY_RULE},
interface::FeeRate,
ViewPair, GuaranteedViewPair, OutputWithDecoys,
};
mod tx_keys;
pub use tx_keys::TransactionKeys;
mod tx;
mod eventuality;
pub use eventuality::Eventuality;
#[cfg(feature = "multisig")]
mod multisig;
#[cfg(feature = "multisig")]
pub use multisig::{TransactionMachine, TransactionSignMachine, TransactionSignatureMachine};
pub(crate) fn key_image_sort(x: &CompressedPoint, y: &CompressedPoint) -> core::cmp::Ordering {
x.cmp(y).reverse()
}
#[derive(Clone, Zeroize)]
enum ChangeEnum {
AddressOnly(MoneroAddress),
Standard { view_pair: ViewPair, subaddress: Option<SubaddressIndex> },
Guaranteed { view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex> },
}
impl PartialEq for ChangeEnum {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ChangeEnum::AddressOnly(lhs), ChangeEnum::AddressOnly(rhs)) => lhs == rhs,
(
ChangeEnum::Standard { view_pair: lhs_vp, subaddress: lhs_s },
ChangeEnum::Standard { view_pair: rhs_vp, subaddress: rhs_s },
) => {
bool::from(lhs_vp.spend.ct_eq(&rhs_vp.spend) & lhs_vp.view.ct_eq(&rhs_vp.view)) &&
(lhs_s == rhs_s)
}
(
ChangeEnum::Guaranteed { view_pair: lhs_vp, subaddress: lhs_s },
ChangeEnum::Guaranteed { view_pair: rhs_vp, subaddress: rhs_s },
) => {
bool::from(lhs_vp.0.spend.ct_eq(&rhs_vp.0.spend) & lhs_vp.0.view.ct_eq(&rhs_vp.0.view)) &&
(lhs_s == rhs_s)
}
_ => false,
}
}
}
impl Eq for ChangeEnum {}
impl ChangeEnum {
fn address(&self) -> MoneroAddress {
match self {
ChangeEnum::AddressOnly(addr) => *addr,
ChangeEnum::Standard { view_pair, subaddress } => match subaddress {
Some(subaddress) => view_pair.subaddress(Network::Mainnet, *subaddress),
None => view_pair.legacy_address(Network::Mainnet),
},
ChangeEnum::Guaranteed { view_pair, subaddress } => {
view_pair.address(Network::Mainnet, *subaddress, None)
}
}
}
}
impl fmt::Debug for ChangeEnum {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let kind = match self {
ChangeEnum::AddressOnly(addr) => {
return f.debug_struct("ChangeEnum::AddressOnly").field("0", &addr).finish();
}
ChangeEnum::Standard { .. } => "ChangeEnum::Standard",
ChangeEnum::Guaranteed { .. } => "ChangeEnum::Guaranteed",
};
f.debug_struct(kind).field("0", &self.address()).finish_non_exhaustive()
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct Change(Option<ChangeEnum>);
impl Change {
pub fn new(view_pair: ViewPair, subaddress: Option<SubaddressIndex>) -> Change {
Change(Some(ChangeEnum::Standard { view_pair, subaddress }))
}
pub fn guaranteed(view_pair: GuaranteedViewPair, subaddress: Option<SubaddressIndex>) -> Change {
Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress }))
}
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
Change(address.map(ChangeEnum::AddressOnly))
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
enum InternalPayment {
Payment(MoneroAddress, u64),
Change(ChangeEnum),
}
impl InternalPayment {
fn address(&self) -> MoneroAddress {
match self {
InternalPayment::Payment(addr, _) => *addr,
InternalPayment::Change(change) => change.address(),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
pub enum SendError {
#[error("this library doesn't yet support that RctType")]
UnsupportedRctType,
#[error("no inputs")]
NoInputs,
#[error("invalid inputs")]
InvalidInputs,
#[error("invalid number of decoys")]
InvalidDecoyQuantity,
#[error("no outputs")]
NoOutputs,
#[error("too many outputs")]
TooManyOutputs,
#[error("only one output and no change address")]
NoChange,
#[error("multiple addresses with payment IDs")]
MultiplePaymentIds,
#[error("too much data")]
TooMuchArbitraryData,
#[error("too large of a transaction")]
TooLargeTransaction,
#[error("transaction amounts exceed u64::MAX (in {in_amount}, out {out_amount})")]
AmountsUnrepresentable {
in_amount: u128,
out_amount: u128,
},
#[error(
"not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
)]
NotEnoughFunds {
inputs: u64,
outputs: u64,
necessary_fee: Option<u64>,
},
#[error("wrong spend private key")]
WrongPrivateKey,
#[error("this SignableTransaction was created by deserializing an incorrect serialization")]
IncorrectSerialization,
#[error("clsag error ({0})")]
ClsagError(ClsagError),
#[cfg(feature = "multisig")]
#[error("frost error {0}")]
FrostError(FrostError),
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct SignableTransaction {
rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<OutputWithDecoys>,
payments: Vec<InternalPayment>,
data: Vec<Vec<u8>>,
fee_rate: FeeRate,
}
impl PartialEq for SignableTransaction {
fn eq(&self, other: &Self) -> bool {
(self.rct_type == other.rct_type) &&
bool::from(self.outgoing_view_key.deref().ct_eq(other.outgoing_view_key.deref())) &&
(self.inputs == other.inputs) &&
(self.payments == other.payments) &&
(self.data == other.data) &&
(self.fee_rate == other.fee_rate)
}
}
impl Eq for SignableTransaction {}
impl fmt::Debug for SignableTransaction {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("SignableTransaction")
.field("rct_type", &self.rct_type)
.field("inputs", &self.inputs)
.field("payments", &self.payments)
.field("data", &self.data)
.field("fee_rate", &self.fee_rate)
.finish_non_exhaustive()
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
struct SignableTransactionWithKeyImages {
intent: SignableTransaction,
key_images: Vec<CompressedPoint>,
}
impl SignableTransaction {
fn validate(&self) -> Result<(), SendError> {
match self.rct_type {
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
RctType::AggregateMlsagBorromean |
RctType::MlsagBorromean |
RctType::MlsagBulletproofs |
RctType::MlsagBulletproofsCompactAmount => Err(SendError::UnsupportedRctType)?,
}
if self.inputs.is_empty() {
Err(SendError::NoInputs)?;
}
if self.inputs.iter().map(|input| input.key().compress()).collect::<HashSet<_>>().len() !=
self.inputs.len()
{
Err(SendError::InvalidInputs)?;
}
for input in &self.inputs {
{
let key = input.key().into();
if !key.is_torsion_free() {
Err(SendError::InvalidInputs)?;
}
use curve25519_dalek::traits::IsIdentity as _;
if key.is_identity() {
Err(SendError::InvalidInputs)?;
}
}
if input.decoys().len() !=
match self.rct_type {
RctType::ClsagBulletproof => 11,
RctType::ClsagBulletproofPlus => 16,
RctType::AggregateMlsagBorromean |
RctType::MlsagBorromean |
RctType::MlsagBulletproofs |
RctType::MlsagBulletproofsCompactAmount => panic!("unsupported RctType"),
}
{
Err(SendError::InvalidDecoyQuantity)?;
}
}
if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
Err(SendError::NoOutputs)?;
}
if self.payments.len() < 2 {
Err(SendError::NoChange)?;
}
{
let mut change_count = 0;
for payment in &self.payments {
change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_))));
}
if change_count > 1 {
Err(SendError::IncorrectSerialization)?;
}
}
{
let mut payment_ids = 0;
for payment in &self.payments {
payment_ids += usize::from(u8::from(payment.address().payment_id().is_some()));
}
if payment_ids > 1 {
Err(SendError::MultiplePaymentIds)?;
}
}
if self.payments.len() > MAX_BULLETPROOF_COMMITMENTS {
Err(SendError::TooManyOutputs)?;
}
for part in &self.data {
if part.len() > MAX_ARBITRARY_DATA_SIZE {
Err(SendError::TooMuchArbitraryData)?;
}
}
if self.extra().len() > MAX_EXTRA_SIZE_BY_RELAY_RULE {
Err(SendError::TooMuchArbitraryData)?;
}
let weight;
{
let in_amount: u128 =
self.inputs.iter().map(|input| u128::from(input.commitment().amount)).sum();
let payments_amount: u128 = self
.payments
.iter()
.filter_map(|payment| match payment {
InternalPayment::Payment(_, amount) => Some(u128::from(*amount)),
InternalPayment::Change(_) => None,
})
.sum();
let necessary_fee;
(weight, necessary_fee) = self.weight_and_necessary_fee();
let out_amount = payments_amount.saturating_add(necessary_fee);
let in_out_amount = u64::try_from(in_amount)
.and_then(|in_amount| u64::try_from(out_amount).map(|out_amount| (in_amount, out_amount)));
let Ok((in_amount, out_amount)) = in_out_amount else {
Err(SendError::AmountsUnrepresentable { in_amount, out_amount })?
};
if in_amount < out_amount {
Err(SendError::NotEnoughFunds {
inputs: in_amount,
outputs: u64::try_from(payments_amount)
.expect("total out fit within u64 but not payments' part of total out"),
necessary_fee: Some(
u64::try_from(necessary_fee)
.expect("total out fit within u64 but not fee's part of total out"),
),
})?;
}
}
const MAX_TX_SIZE: usize = (300_000 / 2) - 600;
if weight >= MAX_TX_SIZE {
Err(SendError::TooLargeTransaction)?;
}
Ok(())
}
pub fn new(
rct_type: RctType,
outgoing_view_key: Zeroizing<[u8; 32]>,
inputs: Vec<OutputWithDecoys>,
payments: Vec<(MoneroAddress, u64)>,
change: Change,
data: Vec<Vec<u8>>,
fee_rate: FeeRate,
) -> Result<SignableTransaction, SendError> {
let mut payments = payments
.into_iter()
.map(|(addr, amount)| InternalPayment::Payment(addr, amount))
.collect::<Vec<_>>();
if let Some(change) = change.0 {
payments.push(InternalPayment::Change(change));
}
let mut res =
SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
res.validate()?;
{
let mut rng = res.seeded_rng(b"shuffle_payments");
res.payments.shuffle(&mut rng);
}
Ok(res)
}
pub fn fee_rate(&self) -> FeeRate {
self.fee_rate
}
pub fn necessary_fee(&self) -> u64 {
u64::try_from(self.weight_and_necessary_fee().1).unwrap()
}
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
fn write_payment<W: io::Write>(payment: &InternalPayment, w: &mut W) -> io::Result<()> {
match payment {
InternalPayment::Payment(addr, amount) => {
w.write_all(&[0])?;
write_vec(write_byte, addr.to_string().as_bytes(), w)?;
w.write_all(&amount.to_le_bytes())
}
InternalPayment::Change(change) => match change {
ChangeEnum::AddressOnly(addr) => {
w.write_all(&[1])?;
write_vec(write_byte, addr.to_string().as_bytes(), w)
}
ChangeEnum::Standard { view_pair, subaddress } => {
w.write_all(&[2])?;
view_pair.spend().compress().write(w)?;
view_pair.view.write(w)?;
if let Some(subaddress) = subaddress {
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())
} else {
w.write_all(&0u32.to_le_bytes())?;
w.write_all(&0u32.to_le_bytes())
}
}
ChangeEnum::Guaranteed { view_pair, subaddress } => {
w.write_all(&[3])?;
view_pair.spend().compress().write(w)?;
view_pair.0.view.write(w)?;
if let Some(subaddress) = subaddress {
w.write_all(&subaddress.account().to_le_bytes())?;
w.write_all(&subaddress.address().to_le_bytes())
} else {
w.write_all(&0u32.to_le_bytes())?;
w.write_all(&0u32.to_le_bytes())
}
}
},
}
}
write_byte(&u8::from(self.rct_type), w)?;
w.write_all(self.outgoing_view_key.as_slice())?;
write_vec(OutputWithDecoys::write, &self.inputs, w)?;
write_vec(write_payment, &self.payments, w)?;
write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?;
self.fee_rate.write(w)
}
pub fn serialize(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
self.write(&mut buf).expect("write failed but <Vec as io::Write> doesn't fail");
buf
}
pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
String::from_utf8(read_vec(read_byte, Some(MoneroAddress::SIZE_UPPER_BOUND.0), r)?)
.ok()
.and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok())
.ok_or_else(|| io::Error::other("invalid address"))
}
fn read_payment<R: io::Read>(r: &mut R) -> io::Result<InternalPayment> {
Ok(match read_byte(r)? {
0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?),
1 => InternalPayment::Change(ChangeEnum::AddressOnly(read_address(r)?)),
2 => InternalPayment::Change(ChangeEnum::Standard {
view_pair: ViewPair::new(
CompressedPoint::read(r)?
.decompress()
.ok_or_else(|| io::Error::other("`Change` payment had invalid public spend key"))?,
Zeroizing::new(Scalar::read(r)?),
)
.map_err(io::Error::other)?,
subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
}),
3 => InternalPayment::Change(ChangeEnum::Guaranteed {
view_pair: GuaranteedViewPair::new(
CompressedPoint::read(r)?.decompress().ok_or_else(|| {
io::Error::other("guaranteed `Change` payment had invalid public spend key")
})?,
Zeroizing::new(Scalar::read(r)?),
)
.map_err(io::Error::other)?,
subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?),
}),
_ => Err(io::Error::other("invalid payment"))?,
})
}
let res = SignableTransaction {
rct_type: RctType::try_from(read_byte(r)?)
.map_err(|()| io::Error::other("unsupported/invalid RctType"))?,
outgoing_view_key: Zeroizing::new(read_bytes(r)?),
inputs: read_vec(OutputWithDecoys::read, Some(TransactionPrefix::INPUTS_UPPER_BOUND.0), r)?,
payments: read_vec(read_payment, Some(MAX_BULLETPROOF_COMMITMENTS), r)?,
data: read_vec(
|r| read_vec(read_byte, Some(MAX_ARBITRARY_DATA_SIZE), r),
Some(MAX_EXTRA_SIZE_BY_RELAY_RULE),
r,
)?,
fee_rate: FeeRate::read(r)?,
};
match res.validate() {
Ok(()) => {}
Err(e) => Err(io::Error::other(e))?,
}
Ok(res)
}
fn with_key_images(
mut self,
mut key_images: Vec<CompressedPoint>,
) -> SignableTransactionWithKeyImages {
debug_assert_eq!(self.inputs.len(), key_images.len());
let mut sorted_inputs = self.inputs.drain(..).zip(key_images.drain(..)).collect::<Vec<_>>();
sorted_inputs
.sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
for (input, key_image) in sorted_inputs {
self.inputs.push(input);
key_images.push(key_image);
}
SignableTransactionWithKeyImages { intent: self, key_images }
}
pub fn unsigned_transaction(self, key_images: Vec<CompressedPoint>) -> Option<Transaction> {
if self.inputs.len() != key_images.len() {
None?;
}
Some(self.with_key_images(key_images).transaction_without_signatures())
}
pub fn sign(
self,
rng: &mut (impl RngCore + CryptoRng),
sender_spend_key: &Zeroizing<Scalar>,
) -> Result<Transaction, SendError> {
let sender_spend_key = Zeroizing::new((**sender_spend_key).into());
let mut key_images = vec![];
for input in &self.inputs {
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset().into());
if bool::from(!(input_key.deref() * ED25519_BASEPOINT_TABLE).ct_eq(&input.key().into())) {
Err(SendError::WrongPrivateKey)?;
}
let key_image = Point::from(
input_key.deref() * Point::biased_hash(input.key().compress().to_bytes()).into(),
);
key_images.push(key_image.compress());
}
let tx = self.with_key_images(key_images);
let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
for input in &tx.intent.inputs {
let input_key =
Zeroizing::new(Scalar::from(sender_spend_key.deref() + input.key_offset().into()));
clsag_signs.push((
input_key,
ClsagContext::new(input.decoys().clone(), input.commitment().clone())
.map_err(SendError::ClsagError)?,
));
}
let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
let mut tx = tx.transaction_without_signatures();
let clsags_and_pseudo_outs = Clsag::sign(
rng,
clsag_signs,
mask_sum,
tx.signature_hash().expect("signing a transaction which isn't signed?"),
)
.map_err(SendError::ClsagError)?;
let inputs_len = tx.prefix().inputs.len();
let Transaction::V2 {
proofs:
Some(RctProofs {
prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. },
..
}),
..
} = tx
else {
panic!("not signing clsag?")
};
*clsags = Vec::with_capacity(inputs_len);
*pseudo_outs = Vec::with_capacity(inputs_len);
for (clsag, pseudo_out) in clsags_and_pseudo_outs {
clsags.push(clsag);
pseudo_outs.push(pseudo_out.compress());
}
Ok(tx)
}
}