mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
584 lines
19 KiB
Rust
584 lines
19 KiB
Rust
|
|
use core::{ops::Deref, fmt};
|
||
|
|
use std_shims::{
|
||
|
|
io, vec,
|
||
|
|
vec::Vec,
|
||
|
|
string::{String, ToString},
|
||
|
|
};
|
||
|
|
|
||
|
|
use zeroize::{Zeroize, Zeroizing};
|
||
|
|
|
||
|
|
use rand_core::{RngCore, CryptoRng};
|
||
|
|
use rand::seq::SliceRandom;
|
||
|
|
|
||
|
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint};
|
||
|
|
#[cfg(feature = "multisig")]
|
||
|
|
use frost::FrostError;
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
io::*,
|
||
|
|
generators::{MAX_COMMITMENTS, hash_to_point},
|
||
|
|
primitives::Decoys,
|
||
|
|
ringct::{
|
||
|
|
clsag::{ClsagError, ClsagContext, Clsag},
|
||
|
|
RctType, RctPrunable, RctProofs,
|
||
|
|
},
|
||
|
|
transaction::Transaction,
|
||
|
|
extra::MAX_ARBITRARY_DATA_SIZE,
|
||
|
|
address::{Network, MoneroAddress},
|
||
|
|
rpc::FeeRate,
|
||
|
|
ViewPair, GuaranteedViewPair, WalletOutput,
|
||
|
|
};
|
||
|
|
|
||
|
|
mod tx_keys;
|
||
|
|
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: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering {
|
||
|
|
x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||
|
|
enum ChangeEnum {
|
||
|
|
None,
|
||
|
|
AddressOnly(MoneroAddress),
|
||
|
|
AddressWithView(MoneroAddress, Zeroizing<Scalar>),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Debug for ChangeEnum {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
|
|
match self {
|
||
|
|
ChangeEnum::None => f.debug_struct("ChangeEnum::None").finish_non_exhaustive(),
|
||
|
|
ChangeEnum::AddressOnly(addr) => {
|
||
|
|
f.debug_struct("ChangeEnum::AddressOnly").field("addr", &addr).finish()
|
||
|
|
}
|
||
|
|
ChangeEnum::AddressWithView(addr, _) => {
|
||
|
|
f.debug_struct("ChangeEnum::AddressWithView").field("addr", &addr).finish_non_exhaustive()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Specification for a change output.
|
||
|
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||
|
|
pub struct Change(ChangeEnum);
|
||
|
|
|
||
|
|
impl Change {
|
||
|
|
/// Create a change output specification.
|
||
|
|
///
|
||
|
|
/// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
|
||
|
|
/// its wallet protocol accordingly.
|
||
|
|
pub fn new(view: &ViewPair) -> Change {
|
||
|
|
Change(ChangeEnum::AddressWithView(
|
||
|
|
// Which network doesn't matter as the derivations will all be the same
|
||
|
|
// TODO: Support subaddresses
|
||
|
|
view.legacy_address(Network::Mainnet),
|
||
|
|
view.view.clone(),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a change output specification for a guaranteed view pair.
|
||
|
|
///
|
||
|
|
/// This take the view key as Monero assumes it has the view key for change outputs. It optimizes
|
||
|
|
/// its wallet protocol accordingly.
|
||
|
|
pub fn guaranteed(view: &GuaranteedViewPair) -> Change {
|
||
|
|
Change(ChangeEnum::AddressWithView(
|
||
|
|
view.address(
|
||
|
|
// Which network doesn't matter as the derivations will all be the same
|
||
|
|
Network::Mainnet,
|
||
|
|
// TODO: Support subaddresses
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
),
|
||
|
|
view.0.view.clone(),
|
||
|
|
))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a fingerprintable change output specification.
|
||
|
|
///
|
||
|
|
/// You MUST assume this will harm your privacy. Only use this if you know what you're doing.
|
||
|
|
///
|
||
|
|
/// If the change address is Some, this will be unable to optimize the transaction as the
|
||
|
|
/// Monero wallet protocol expects it can (due to presumably having the view key for the change
|
||
|
|
/// output). If a transaction should be optimized, and isn'tm it will be fingerprintable.
|
||
|
|
///
|
||
|
|
/// If the change address is None, there are two fingerprints:
|
||
|
|
///
|
||
|
|
/// 1) The change in the TX is shunted to the fee (making it fingerprintable).
|
||
|
|
///
|
||
|
|
/// 2) If there are two outputs in the TX, Monero would create a payment ID for the non-change
|
||
|
|
/// output so an observer can't tell apart TXs with a payment ID from TXs without a payment
|
||
|
|
/// ID. monero-wallet will simply not create a payment ID in this case, revealing it's a
|
||
|
|
/// monero-wallet TX without change.
|
||
|
|
pub fn fingerprintable(address: Option<MoneroAddress>) -> Change {
|
||
|
|
if let Some(address) = address {
|
||
|
|
Change(ChangeEnum::AddressOnly(address))
|
||
|
|
} else {
|
||
|
|
Change(ChangeEnum::None)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Clone, PartialEq, Eq, Zeroize)]
|
||
|
|
enum InternalPayment {
|
||
|
|
Payment(MoneroAddress, u64),
|
||
|
|
Change(MoneroAddress, Option<Zeroizing<Scalar>>),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl InternalPayment {
|
||
|
|
fn address(&self) -> &MoneroAddress {
|
||
|
|
match self {
|
||
|
|
InternalPayment::Payment(addr, _) | InternalPayment::Change(addr, _) => addr,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl fmt::Debug for InternalPayment {
|
||
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
|
|
match self {
|
||
|
|
InternalPayment::Payment(addr, amount) => f
|
||
|
|
.debug_struct("InternalPayment::Payment")
|
||
|
|
.field("addr", &addr)
|
||
|
|
.field("amount", &amount)
|
||
|
|
.finish(),
|
||
|
|
InternalPayment::Change(addr, _) => {
|
||
|
|
f.debug_struct("InternalPayment::Change").field("addr", &addr).finish_non_exhaustive()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// An error while sending Monero.
|
||
|
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||
|
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||
|
|
pub enum SendError {
|
||
|
|
/// The RingCT type to produce proofs for this transaction with weren't supported.
|
||
|
|
#[cfg_attr(feature = "std", error("this library doesn't yet support that RctType"))]
|
||
|
|
UnsupportedRctType,
|
||
|
|
/// The transaction had no inputs specified.
|
||
|
|
#[cfg_attr(feature = "std", error("no inputs"))]
|
||
|
|
NoInputs,
|
||
|
|
/// The decoy quantity was invalid for the specified RingCT type.
|
||
|
|
#[cfg_attr(feature = "std", error("invalid number of decoys"))]
|
||
|
|
InvalidDecoyQuantity,
|
||
|
|
/// The transaction had no outputs specified.
|
||
|
|
#[cfg_attr(feature = "std", error("no outputs"))]
|
||
|
|
NoOutputs,
|
||
|
|
/// The transaction had too many outputs specified.
|
||
|
|
#[cfg_attr(feature = "std", error("too many outputs"))]
|
||
|
|
TooManyOutputs,
|
||
|
|
/// The transaction did not have a change output, and did not have two outputs.
|
||
|
|
///
|
||
|
|
/// Monero requires all transactions have at least two outputs, assuming one payment and one
|
||
|
|
/// change (or at least one dummy and one change). Accordingly, specifying no change and only
|
||
|
|
/// one payment prevents creating a valid transaction
|
||
|
|
#[cfg_attr(feature = "std", error("only one output and no change address"))]
|
||
|
|
NoChange,
|
||
|
|
/// Multiple addresses had payment IDs specified.
|
||
|
|
///
|
||
|
|
/// Only one payment ID is allowed per transaction.
|
||
|
|
#[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))]
|
||
|
|
MultiplePaymentIds,
|
||
|
|
/// Too much arbitrary data was specified.
|
||
|
|
#[cfg_attr(feature = "std", error("too much data"))]
|
||
|
|
TooMuchArbitraryData,
|
||
|
|
/// The created transaction was too large.
|
||
|
|
#[cfg_attr(feature = "std", error("too large of a transaction"))]
|
||
|
|
TooLargeTransaction,
|
||
|
|
/// This transaction could not pay for itself.
|
||
|
|
#[cfg_attr(
|
||
|
|
feature = "std",
|
||
|
|
error(
|
||
|
|
"not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})"
|
||
|
|
)
|
||
|
|
)]
|
||
|
|
NotEnoughFunds {
|
||
|
|
/// The amount of funds the inputs contributed.
|
||
|
|
inputs: u64,
|
||
|
|
/// The amount of funds the outputs required.
|
||
|
|
outputs: u64,
|
||
|
|
/// The fee necessary to be paid on top.
|
||
|
|
///
|
||
|
|
/// If this is None, it is because the fee was not calculated as the outputs alone caused this
|
||
|
|
/// error.
|
||
|
|
necessary_fee: Option<u64>,
|
||
|
|
},
|
||
|
|
/// This transaction is being signed with the wrong private key.
|
||
|
|
#[cfg_attr(feature = "std", error("wrong spend private key"))]
|
||
|
|
WrongPrivateKey,
|
||
|
|
/// This transaction was read from a bytestream which was malicious.
|
||
|
|
#[cfg_attr(
|
||
|
|
feature = "std",
|
||
|
|
error("this SignableTransaction was created by deserializing a malicious serialization")
|
||
|
|
)]
|
||
|
|
MaliciousSerialization,
|
||
|
|
/// There was an error when working with the CLSAGs.
|
||
|
|
#[cfg_attr(feature = "std", error("clsag error ({0})"))]
|
||
|
|
ClsagError(ClsagError),
|
||
|
|
/// There was an error when working with FROST.
|
||
|
|
#[cfg(feature = "multisig")]
|
||
|
|
#[cfg_attr(feature = "std", error("frost error {0}"))]
|
||
|
|
FrostError(FrostError),
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A signable transaction.
|
||
|
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||
|
|
pub struct SignableTransaction {
|
||
|
|
rct_type: RctType,
|
||
|
|
outgoing_view_key: Zeroizing<[u8; 32]>,
|
||
|
|
inputs: Vec<(WalletOutput, Decoys)>,
|
||
|
|
payments: Vec<InternalPayment>,
|
||
|
|
data: Vec<Vec<u8>>,
|
||
|
|
fee_rate: FeeRate,
|
||
|
|
}
|
||
|
|
|
||
|
|
struct SignableTransactionWithKeyImages {
|
||
|
|
intent: SignableTransaction,
|
||
|
|
key_images: Vec<EdwardsPoint>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl SignableTransaction {
|
||
|
|
fn validate(&self) -> Result<(), SendError> {
|
||
|
|
match self.rct_type {
|
||
|
|
RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {}
|
||
|
|
_ => Err(SendError::UnsupportedRctType)?,
|
||
|
|
}
|
||
|
|
|
||
|
|
if self.inputs.is_empty() {
|
||
|
|
Err(SendError::NoInputs)?;
|
||
|
|
}
|
||
|
|
for (_, decoys) in &self.inputs {
|
||
|
|
// TODO: Add a function for the ring length
|
||
|
|
if decoys.len() !=
|
||
|
|
match self.rct_type {
|
||
|
|
RctType::ClsagBulletproof => 11,
|
||
|
|
RctType::ClsagBulletproofPlus => 16,
|
||
|
|
_ => panic!("unsupported RctType"),
|
||
|
|
}
|
||
|
|
{
|
||
|
|
Err(SendError::InvalidDecoyQuantity)?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check we have at least one non-change output
|
||
|
|
if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) {
|
||
|
|
Err(SendError::NoOutputs)?;
|
||
|
|
}
|
||
|
|
// If we don't have at least two outputs, as required by Monero, error
|
||
|
|
if self.payments.len() < 2 {
|
||
|
|
Err(SendError::NoChange)?;
|
||
|
|
}
|
||
|
|
// Check we don't have multiple Change outputs due to decoding a malicious serialization
|
||
|
|
{
|
||
|
|
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::MaliciousSerialization)?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make sure there's at most one payment ID
|
||
|
|
{
|
||
|
|
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_COMMITMENTS {
|
||
|
|
Err(SendError::TooManyOutputs)?;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the length of each arbitrary data
|
||
|
|
for part in &self.data {
|
||
|
|
if part.len() > MAX_ARBITRARY_DATA_SIZE {
|
||
|
|
Err(SendError::TooMuchArbitraryData)?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the length of TX extra
|
||
|
|
// https://github.com/monero-project/monero/pull/8733
|
||
|
|
const MAX_EXTRA_SIZE: usize = 1060;
|
||
|
|
if self.extra().len() > MAX_EXTRA_SIZE {
|
||
|
|
Err(SendError::TooMuchArbitraryData)?;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make sure we have enough funds
|
||
|
|
let in_amount = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::<u64>();
|
||
|
|
let payments_amount = self
|
||
|
|
.payments
|
||
|
|
.iter()
|
||
|
|
.filter_map(|payment| match payment {
|
||
|
|
InternalPayment::Payment(_, amount) => Some(amount),
|
||
|
|
InternalPayment::Change(_, _) => None,
|
||
|
|
})
|
||
|
|
.sum::<u64>();
|
||
|
|
let (weight, necessary_fee) = self.weight_and_necessary_fee();
|
||
|
|
if in_amount < (payments_amount + necessary_fee) {
|
||
|
|
Err(SendError::NotEnoughFunds {
|
||
|
|
inputs: in_amount,
|
||
|
|
outputs: payments_amount,
|
||
|
|
necessary_fee: Some(necessary_fee),
|
||
|
|
})?;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The actual limit is half the block size, and for the minimum block size of 300k, that'd be
|
||
|
|
// 150k
|
||
|
|
// wallet2 will only create transactions up to 100k bytes however
|
||
|
|
// TODO: Cite
|
||
|
|
const MAX_TX_SIZE: usize = 100_000;
|
||
|
|
if weight >= MAX_TX_SIZE {
|
||
|
|
Err(SendError::TooLargeTransaction)?;
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a new SignableTransaction.
|
||
|
|
///
|
||
|
|
/// `outgoing_view_key` is used to seed the RNGs for this transaction. Anyone with knowledge of
|
||
|
|
/// the outgoing view key will be able to identify a transaction produced with this methodology,
|
||
|
|
/// and the data within it. Accordingly, it must be treated as a private key.
|
||
|
|
///
|
||
|
|
/// `data` represents arbitrary data which will be embedded into the transaction's `extra` field.
|
||
|
|
/// The embedding occurs using an `ExtraField::Nonce` with a custom marker byte (as to not
|
||
|
|
/// conflict with a payment ID).
|
||
|
|
pub fn new(
|
||
|
|
rct_type: RctType,
|
||
|
|
outgoing_view_key: Zeroizing<[u8; 32]>,
|
||
|
|
inputs: Vec<(WalletOutput, Decoys)>,
|
||
|
|
payments: Vec<(MoneroAddress, u64)>,
|
||
|
|
change: Change,
|
||
|
|
data: Vec<Vec<u8>>,
|
||
|
|
fee_rate: FeeRate,
|
||
|
|
) -> Result<SignableTransaction, SendError> {
|
||
|
|
// Re-format the payments and change into a consolidated payments list
|
||
|
|
let mut payments = payments
|
||
|
|
.into_iter()
|
||
|
|
.map(|(addr, amount)| InternalPayment::Payment(addr, amount))
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
match change.0 {
|
||
|
|
ChangeEnum::None => {}
|
||
|
|
ChangeEnum::AddressOnly(addr) => payments.push(InternalPayment::Change(addr, None)),
|
||
|
|
ChangeEnum::AddressWithView(addr, view) => {
|
||
|
|
payments.push(InternalPayment::Change(addr, Some(view)))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut res =
|
||
|
|
SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate };
|
||
|
|
res.validate()?;
|
||
|
|
|
||
|
|
// Shuffle the payments
|
||
|
|
{
|
||
|
|
let mut rng = res.seeded_rng(b"shuffle_payments");
|
||
|
|
res.payments.shuffle(&mut rng);
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(res)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The fee rate this transaction uses.
|
||
|
|
pub fn fee_rate(&self) -> FeeRate {
|
||
|
|
self.fee_rate
|
||
|
|
}
|
||
|
|
|
||
|
|
/// The fee this transaction requires.
|
||
|
|
///
|
||
|
|
/// This is distinct from the fee this transaction will use. If no change output is specified,
|
||
|
|
/// all unspent coins will be shunted to the fee.
|
||
|
|
pub fn necessary_fee(&self) -> u64 {
|
||
|
|
self.weight_and_necessary_fee().1
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Write a SignableTransaction.
|
||
|
|
///
|
||
|
|
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
||
|
|
/// defined serialization.
|
||
|
|
pub fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
||
|
|
fn write_input<W: io::Write>(input: &(WalletOutput, Decoys), w: &mut W) -> io::Result<()> {
|
||
|
|
input.0.write(w)?;
|
||
|
|
input.1.write(w)
|
||
|
|
}
|
||
|
|
|
||
|
|
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(addr, change_view) => {
|
||
|
|
w.write_all(&[1])?;
|
||
|
|
write_vec(write_byte, addr.to_string().as_bytes(), w)?;
|
||
|
|
if let Some(view) = change_view.as_ref() {
|
||
|
|
w.write_all(&[1])?;
|
||
|
|
write_scalar(view, w)
|
||
|
|
} else {
|
||
|
|
w.write_all(&[0])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
write_byte(&u8::from(self.rct_type), w)?;
|
||
|
|
w.write_all(self.outgoing_view_key.as_slice())?;
|
||
|
|
write_vec(write_input, &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)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Serialize the SignableTransaction to a `Vec<u8>`.
|
||
|
|
///
|
||
|
|
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
||
|
|
/// defined serialization.
|
||
|
|
pub fn serialize(&self) -> Vec<u8> {
|
||
|
|
let mut buf = Vec::with_capacity(256);
|
||
|
|
self.write(&mut buf).unwrap();
|
||
|
|
buf
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Read a `SignableTransaction`.
|
||
|
|
///
|
||
|
|
/// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol
|
||
|
|
/// defined serialization.
|
||
|
|
pub fn read<R: io::Read>(r: &mut R) -> io::Result<SignableTransaction> {
|
||
|
|
fn read_input(r: &mut impl io::Read) -> io::Result<(WalletOutput, Decoys)> {
|
||
|
|
Ok((WalletOutput::read(r)?, Decoys::read(r)?))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn read_address<R: io::Read>(r: &mut R) -> io::Result<MoneroAddress> {
|
||
|
|
String::from_utf8(read_vec(read_byte, 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(
|
||
|
|
read_address(r)?,
|
||
|
|
match read_byte(r)? {
|
||
|
|
0 => None,
|
||
|
|
1 => Some(Zeroizing::new(read_scalar(r)?)),
|
||
|
|
_ => Err(io::Error::other("invalid change view"))?,
|
||
|
|
},
|
||
|
|
),
|
||
|
|
_ => 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(read_input, r)?,
|
||
|
|
payments: read_vec(read_payment, r)?,
|
||
|
|
data: read_vec(|r| read_vec(read_byte, r), 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, key_images: Vec<EdwardsPoint>) -> SignableTransactionWithKeyImages {
|
||
|
|
debug_assert_eq!(self.inputs.len(), key_images.len());
|
||
|
|
|
||
|
|
// Sort the inputs by their key images
|
||
|
|
let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::<Vec<_>>();
|
||
|
|
sorted_inputs
|
||
|
|
.sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b));
|
||
|
|
|
||
|
|
self.inputs = Vec::with_capacity(sorted_inputs.len());
|
||
|
|
let mut key_images = Vec::with_capacity(sorted_inputs.len());
|
||
|
|
for (input, key_image) in sorted_inputs {
|
||
|
|
self.inputs.push(input);
|
||
|
|
key_images.push(key_image);
|
||
|
|
}
|
||
|
|
|
||
|
|
SignableTransactionWithKeyImages { intent: self, key_images }
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Sign this transaction.
|
||
|
|
pub fn sign(
|
||
|
|
self,
|
||
|
|
rng: &mut (impl RngCore + CryptoRng),
|
||
|
|
sender_spend_key: &Zeroizing<Scalar>,
|
||
|
|
) -> Result<Transaction, SendError> {
|
||
|
|
// Calculate the key images
|
||
|
|
let mut key_images = vec![];
|
||
|
|
for (input, _) in &self.inputs {
|
||
|
|
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
|
||
|
|
if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() {
|
||
|
|
Err(SendError::WrongPrivateKey)?;
|
||
|
|
}
|
||
|
|
let key_image = input_key.deref() * hash_to_point(input.key().compress().to_bytes());
|
||
|
|
key_images.push(key_image);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert to a SignableTransactionWithKeyImages
|
||
|
|
let tx = self.with_key_images(key_images);
|
||
|
|
|
||
|
|
// Prepare the CLSAG signatures
|
||
|
|
let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len());
|
||
|
|
for (input, decoys) in &tx.intent.inputs {
|
||
|
|
// Re-derive the input key as this will be in a different order
|
||
|
|
let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset());
|
||
|
|
clsag_signs.push((
|
||
|
|
input_key,
|
||
|
|
ClsagContext::new(decoys.clone(), input.commitment().clone())
|
||
|
|
.map_err(SendError::ClsagError)?,
|
||
|
|
));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the output commitments' mask sum
|
||
|
|
let mask_sum = tx.intent.sum_output_masks(&tx.key_images);
|
||
|
|
|
||
|
|
// Get the actual TX, just needing the CLSAGs
|
||
|
|
let mut tx = tx.transaction_without_signatures();
|
||
|
|
|
||
|
|
// Sign the CLSAGs
|
||
|
|
let clsags_and_pseudo_outs =
|
||
|
|
Clsag::sign(rng, clsag_signs, mask_sum, tx.signature_hash().unwrap())
|
||
|
|
.map_err(SendError::ClsagError)?;
|
||
|
|
|
||
|
|
// Fill in the CLSAGs/pseudo-outs
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return the signed TX
|
||
|
|
Ok(tx)
|
||
|
|
}
|
||
|
|
}
|